<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>cannon.js + three.js - cloth simulation</title>
    <link rel="stylesheet" href="css/style.css" type="text/css" />
    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" />
    <script type="module">
      import { addTitle, addSourceButton } from './js/dom-utils.js'

      addTitle()
      addSourceButton()
    </script>
  </head>

  <body>
    <script type="module">
      import * as CANNON from '../dist/cannon-es.js'
      import * as THREE from 'https://unpkg.com/three@0.122.0/build/three.module.js'
      import Stats from 'https://unpkg.com/three@0.122.0/examples/jsm/libs/stats.module.js'
      import { OrbitControls } from 'https://unpkg.com/three@0.122.0/examples/jsm/controls/OrbitControls.js'

      /**
       * Example on how to build a simple physics cloth with
       * just structurals constraints.
       * More info here:
       * https://viscomp.alexandra.dk/?p=147
       */

      // Specify the simulation constants
      const timeStep = 1 / 60
      let lastCallTime

      const clothMass = 1 // 1 kg in total
      const clothSize = 1 // 1 meter
      const Nx = 12 // number of horizontal particles in the cloth
      const Ny = 12 // number of vertical particles in the cloth
      const mass = (clothMass / Nx) * Ny
      const restDistance = clothSize / Nx

      const sphereSize = 0.1
      const movementRadius = 0.2

      // Parametric function
      // https://threejs.org/docs/index.html#api/en/geometries/ParametricGeometry
      function clothFunction(u, v, target) {
        const x = (u - 0.5) * restDistance * Nx
        const y = (v + 0.5) * restDistance * Ny
        const z = 0

        target.set(x, y, z)

        return target
      }

      // three.js variables
      let camera, scene, renderer, stats
      let controls
      let clothGeometry
      let sphereMesh

      // cannon.js variables
      let world
      let sphereBody
      const particles = []

      initCannon()
      initThree()
      animate()

      function initCannon() {
        world = new CANNON.World()
        world.gravity.set(0, -9.81, 0)

        // Max solver iterations: Use more for better force propagation, but keep in mind that it's not very computationally cheap!
        world.solver.iterations = 20

        // Materials
        const clothMaterial = new CANNON.Material('cloth')
        const sphereMaterial = new CANNON.Material('sphere')
        const cloth_sphere = new CANNON.ContactMaterial(clothMaterial, sphereMaterial, {
          friction: 0,
          restitution: 0,
        })
        // Adjust constraint equation parameters
        // Contact stiffness - use to make softer/harder contacts
        cloth_sphere.contactEquationStiffness = 1e9
        // Stabilization time in number of timesteps
        cloth_sphere.contactEquationRelaxation = 3

        // Add contact material to the world
        world.addContactMaterial(cloth_sphere)

        // Create sphere
        // Make it a little bigger than the three.js sphere
        // so the cloth doesn't clip thruogh
        const sphereShape = new CANNON.Sphere(sphereSize * 1.3)
        sphereBody = new CANNON.Body({
          type: CANNON.Body.KINEMATIC,
        })
        sphereBody.addShape(sphereShape)
        world.addBody(sphereBody)

        // Create cannon particles
        for (let i = 0; i < Nx + 1; i++) {
          particles.push([])
          for (let j = 0; j < Ny + 1; j++) {
            const index = j * (Nx + 1) + i

            const point = clothFunction(i / (Nx + 1), j / (Ny + 1), new THREE.Vector3())
            const particle = new CANNON.Body({
              // Fix in place the first row
              mass: j === Ny ? 0 : mass,
            })
            particle.addShape(new CANNON.Particle())
            particle.linearDamping = 0.5
            particle.position.set(point.x, point.y - Ny * 0.9 * restDistance, point.z)
            particle.velocity.set(0, 0, -0.1 * (Ny - j))

            particles[i].push(particle)
            world.addBody(particle)
          }
        }

        // Connect the particles with distance constraints
        function connect(i1, j1, i2, j2) {
          world.addConstraint(new CANNON.DistanceConstraint(particles[i1][j1], particles[i2][j2], restDistance))
        }
        for (let i = 0; i < Nx + 1; i++) {
          for (let j = 0; j < Ny + 1; j++) {
            if (i < Nx) connect(i, j, i + 1, j)
            if (j < Ny) connect(i, j, i, j + 1)
          }
        }
      }

      function initThree() {
        // Camera
        camera = new THREE.PerspectiveCamera(30, window.innerWidth / window.innerHeight, 0.5, 10000)
        camera.position.set(Math.cos(Math.PI / 4) * 3, 0, Math.sin(Math.PI / 4) * 3)

        // Scene
        scene = new THREE.Scene()
        scene.fog = new THREE.Fog(0x000000, 500, 10000)

        // Renderer
        renderer = new THREE.WebGLRenderer({ antialias: true })
        renderer.setSize(window.innerWidth, window.innerHeight)
        renderer.setClearColor(scene.fog.color)

        renderer.outputEncoding = THREE.sRGBEncoding

        document.body.appendChild(renderer.domElement)

        // Stats.js
        stats = new Stats()
        document.body.appendChild(stats.dom)

        // Orbit controls
        controls = new OrbitControls(camera, renderer.domElement)
        controls.enableDamping = true
        controls.enablePan = false
        controls.dampingFactor = 0.3
        controls.minDistance = 1
        controls.maxDistance = 5

        // Lights
        const ambientLight = new THREE.AmbientLight(0xffffff, 0.4)
        scene.add(ambientLight)

        const directionalLight = new THREE.DirectionalLight(0xffffff, 1.75)
        directionalLight.position.set(5, 5, 5)
        scene.add(directionalLight)

        // Cloth material
        const clothTexture = new THREE.TextureLoader().load('images/sunflower.jpg')
        clothTexture.wrapS = THREE.RepeatWrapping
        clothTexture.wrapT = THREE.RepeatWrapping
        clothTexture.anisotropy = 16
        clothTexture.encoding = THREE.sRGBEncoding

        const clothMaterial = new THREE.MeshPhongMaterial({
          map: clothTexture,
          side: THREE.DoubleSide,
        })

        // Cloth geometry
        clothGeometry = new THREE.ParametricGeometry(clothFunction, Nx, Ny)

        // Cloth mesh
        const clothMesh = new THREE.Mesh(clothGeometry, clothMaterial)
        scene.add(clothMesh)

        // Sphere
        const sphereGeometry = new THREE.SphereGeometry(sphereSize, 20, 20)
        const sphereMaterial = new THREE.MeshPhongMaterial({ color: 0x888888 })

        sphereMesh = new THREE.Mesh(sphereGeometry, sphereMaterial)
        scene.add(sphereMesh)

        window.addEventListener('resize', onWindowResize)
      }

      function onWindowResize() {
        camera.aspect = window.innerWidth / window.innerHeight
        camera.updateProjectionMatrix()
        renderer.setSize(window.innerWidth, window.innerHeight)
      }

      function animate() {
        requestAnimationFrame(animate)
        controls.update()
        updatePhysics()
        render()
        stats.update()
      }

      // Step the physics world
      function updatePhysics() {
        const time = performance.now() / 1000
        if (!lastCallTime) {
          world.step(timeStep)
        } else {
          const dt = time - lastCallTime
          world.step(timeStep, dt)
        }
        lastCallTime = time
      }

      function render() {
        // Make the three.js cloth follow the cannon.js particles
        for (let i = 0; i < Nx + 1; i++) {
          for (let j = 0; j < Ny + 1; j++) {
            const index = j * (Nx + 1) + i
            clothGeometry.vertices[index].copy(particles[i][j].position)
          }
        }

        clothGeometry.normalsNeedUpdate = true
        clothGeometry.verticesNeedUpdate = true

        // Move the ball in a circular motion
        const { time } = world
        sphereBody.position.set(movementRadius * Math.sin(time), 0, movementRadius * Math.cos(time))

        // Make the three.js ball follow the cannon.js one
        // Copying quaternion is not needed since it's a sphere
        sphereMesh.position.copy(sphereBody.position)

        renderer.render(scene, camera)
      }
    </script>
  </body>
</html>
